查看原文
其他

MotionLayout:布局中的战斗机 Oyeah!

knight康康 AndroidPub 2022-07-13


MotionLayout 是 ConstraintLayout 的子类,可以基于动画和手势实现效果炫酷的布局切换。
 implementation 'androidx.constraintlayout:constraintlayout:2.0.0'

ConstraintLayout 的 2.0 以上就可以使用 MotionLayout 了,目前最新版是2.1.0-beta02。

将布局转换为MotionLayout

MotionLayout 是ConstraintLayout 的子类,用ConstraintLayout 写的布局,用MotionLayout也可以。
所有就放心的将ConstraintLayout 转换为MotionLayout 吧
打开布局 在视图预览处或则在Component Tree 栏选中选择ConstraintLayout 右击在菜单栏中点击 Covert to MotionLayout 选项


认识 MotionLayout 工作台

当我们通过上面方式将ConstraintLayout转换为MotionLayout 之后,会帮我们创建一个MotionScene文件 在 res/xml/

文件内容如下

<?xml version="1.0" encoding="utf-8"?>
<MotionScene 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:motion="http://schemas.android.com/apk/res-auto">


    <Transition
        motion:constraintSetEnd="@+id/end"
        motion:constraintSetStart="@id/start"
        motion:duration="1000">

       <KeyFrameSet>
       </KeyFrameSet>
    </Transition>

    <ConstraintSet android:id="@+id/start">
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
    </ConstraintSet>
</MotionScene>

这个文件和布局关联在一起是通过在布局文件声明的 app:layoutDescription

<androidx.constraintlayout.motion.widget.MotionLayout 
      ……                                                
    app:layoutDescription="@xml/activity_main_scene"
   >

再来看看布局,Android  studio(4.0及以上版本) 给我带来一个MotionLayout 可视化的动画编辑工具,如下图所示

普通状态,选中后预览视图显示原始的状态
表示id="start"的ConstraintSet,选中后预览视图显示此约束集的布局,xml 中代码如下

  <ConstraintSet android:id="@+id/start">
    </ConstraintSet>

💡   ConstraintSet 标签 官方解释:"指定所有视图在动画序列中某一点上的位置和属性。通常,一个 Transition元素可指向两个 ConstraintSet  元素,其中一个定义动画序列的开始,另一个定义结束" 我的理解 ConstraintSet 用来存放一些View在某个状态下的约束集和属性

表示id="end"的ConstraintSet,选中后预览视图显示此约束集的布局
表示从start 约束集到end 的转场,对应xml 代码如下

 <Transition
        motion:constraintSetEnd="@+id/end"
        motion:constraintSetStart="@id/start"
        motion:duration="1000">

    </Transition>

创建一个新的ConstraintSet
创建一个新的转场Transition
创建触发转场的行为,是点击Click还是滑动swipe

<Transition
        motion:constraintSetEnd="@+id/end"
        motion:constraintSetStart="@id/start"
        motion:duration="1000">

        <!-- 点击-->
        <OnClick />
        <!-- 滑动-->
        <OnSwipe />
    </Transition>

仔细看这三个图标还挺形象的

点击后有有个帮助教程

表示约束集的元素


动画的开始与结束

动画,有开始有结束。我们享受的是从开始到结束的过程。

入门一个简单的效果


预览

实现一个简单的矩形平移动画

💡  上图是运行效果,图中虚线表示运动轨迹。要显示运动轨迹的虚线 需要把MotionLayout的showPaths 属性设置为trueapp:showPaths="true"


创建动画元素

在布局中创建一个View 如下

<androidx.constraintlayout.motion.widget.MotionLayout 
      ……
    app:layoutDescription="@xml/activity_main_scene"
    app:showPaths="true">


     <View
        android:id="@+id/box"
        android:layout_width="64dp"
        android:layout_height="64dp"
        android:background="@color/colorAccent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"/>

  
</androidx.constraintlayout.motion.widget.MotionLayout>


定义动画开始&结束状态(ConstraintSet)

将id="box" 的添加到id="start" 的约束集(ConstraintSet)中
经过上面一波操作,就会在约束集(id="start") 中为View(id="box") 创建一个新的约束。这一波操作对应的xml 代码如下

 <ConstraintSet android:id="@+id/start">
        <Constraint
            android:id="@+id/box"
            android:layout_width="64dp"
            android:layout_height="64dp"
            motion:layout_constraintTop_toTopOf="parent"
            motion:layout_constraintStart_toStartOf="parent" />

    </ConstraintSet>

ConstraintSet上文我们说了,它是存放一些view 约束和属性的的集合,描述View约束和属性是通过Constraint 标签。根据自己需要我们修改Constraint(id="box") 的约束如下(垂直居中,水平方向靠最左边)

  <Constraint
            android:id="@+id/box"
            android:layout_width="64dp"
            android:layout_height="64dp"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintBottom_toBottomOf="parent"
            motion:layout_constraintTop_toTopOf="parent" />

类似的做法,我们在ConstraintSet(id="end") 添加View(id="box") 的约束如下(垂直居中,水平方向靠最右边)

<ConstraintSet android:id="@+id/end">
        <Constraint
            android:id="@+id/box"
            android:layout_width="64dp"
            android:layout_height="64dp"
            motion:layout_constraintBottom_toBottomOf="parent"
            motion:layout_constraintEnd_toEndOf="parent"
            motion:layout_constraintTop_toTopOf="parent" />

    </ConstraintSet>


转场(Transition)

现在View(id="box")start 和 end的约束已经写好了,怎么动起来呢,请看下面代码

   <Transition
        motion:constraintSetEnd="@+id/end"
        motion:constraintSetStart="@id/start"
        motion:duration="1000">

        <!-- 点击-->
        <OnClick />
    </Transition>

Transition 定义可视化就是上图红色区域那条带箭头的线,有没有注意到线上还有个小点就是表示点击

Transition 标签,通过  motion:constraintSetStart 指定运动开始状态(其值是ConstrainSet的id) 到motion:constraintSetEnd 指定运动结束状态 的转场,motion:duration 来指定动画执行的时间。Transition 只是定义了指定开始和结束的状态,要让用户去触发ta,还要在Transition 标签下添加

💡 Transition 有一个属性motion``:autoTransition 可以设置自动执行转场的行为,无需用户自己操作。


OnClick

表示由用户点击触发
属性

  1. motion:targetId="@id/target_view"   (目标View的id)

如果不指定次属性,就是点击整个屏幕触发如果写了这个属性,就是点击对应id的View 触发转场动画

  1. motion:clickAction="action"  点击后要进行的行为 ,此属性可以设置以下几个值

transitionToStart
过渡到 <Transition> 元素 motion::constraintSetStart 属性指定的状态,有过度动画效果
transitionToEnd
过渡到 <Transition> 元素motion:constraintSetEnd 属性指定的状态,有过度动画效果
jumpToStart
直接跳转到 <Transition> 元素 motion::constraintSetStart 属性指定的状态,没有动画效果
jumpToEnd
直接跳转到 <Transition> 元素 motion:constraintSetEnd 属性指定的状态。
toggle
默认值就是这个。在<Transition> 元素motion::constraintSetStartmotion:constraintSetEnd 指定的布局之间切换,如果处于start状态就过度到end状态,如果处于end状态就过度到start状态,有过度动画。


OnSwipe

表示由用户滑动触发,它会根据用户滑动行为调整动画的进度。一个Transition 标签下可以包含多个OnSwipe
示例

   <Transition
        motion:constraintSetEnd="@+id/end"
        motion:constraintSetStart="@id/start"
        motion:duration="1000">

        <OnSwipe
            motion:dragDirection="dragEnd"
            motion:touchRegionId="@+id/box" />

    </Transition>

它有以下几个属性
motion:touchAnchorId  其值为View的Id,滑动后做出相应的视图

💡  OnSwipe 监听的是MotionLayout 的滑动,不是motion:touchAnchorId的滑动

motion:touchAnchorSide
官网解释:滑动所固定到的目标视图的一侧。MotionLayout 将尝试在该固定点与用户手指之间保持恒定的距离。
可接受的值包括 "left""right""top""bottom"
motion:dragDirection
用户滑动动作的方向。如果设置了此属性,此 <onSwipe> 将仅适用于沿特定方向的滑动。可接受的值包括 "dragLeft""dragRight""dragUp""dragDown"

这个属性有意思,可以让目标跟着你手指滑动方向走,可以按你滑动的反方向走,更或者按你滑动的垂直方向走,就看你怎么设置这个属性

motion:dragScale
控制视图相对于滑动长度的移动距离。默认值为 1,表明视图移动的距离应与滑动距离一致。如果 dragScale 小于 1,视图移动的距离会远远小于滑动距离(例如,dragScale 为 0.5 意味着如果滑动移动 4 厘米,目标视图会移动 2 厘米)。如果 dragScale 大于 1,视图移动的距离会大于滑动距离(例如,dragScale 为 1.5 意味着如果滑动移动 4 厘米,目标视图会移动 6 厘米)。
motion:maxVelocity
目标视图的最大速度。当手指滑动一定速度,目标View 会按照惯性继续运作,进行先加速后减速运行(默认情况)。如果运动过程中加速到了我们设置的最大值(根据我们学的初中那点物理知识可知,能不能到达速度最大值,受加速度()、时间()有关、以及初始速度(),),那么就是先加速,然后按最大速度匀速运动,然后在减速运动。
motion:maxAcceleration
目标视图的最大加速度,如果想让View 运动变快,就把加速度调大一点吧

属性转场动画

上面我们说了,ConstraintSet的 Constraint 中可以指定约束和属性,上面的平移的例子中我们只用了约束,下面我们看看加些属性的效果吧。

1.设置透明度属性

</ConstraintSet>

<ConstraintSet android:id="@+id/end">
      <ConstraintSet android:id="@+id/start">
        <Constraint
            android:id="@+id/box"
            android:layout_width="64dp"
            android:layout_height="64dp"
            android:alpha="1"
            motion:layout_constraintBottom_toBottomOf="parent"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintTop_toTopOf="parent" />

    </ConstraintSet>
  
  <ConstraintSet android:id="@+id/end">
        <Constraint
            android:id="@+id/box"
            android:layout_width="64dp"
            android:layout_height="64dp"
            android:alpha="0.1"
            motion:layout_constraintBottom_toBottomOf="parent"
            motion:layout_constraintEnd_toEndOf="parent"
            motion:layout_constraintTop_toTopOf="parent" />

    </ConstraintSet>
</ConstraintSet>

在动画开始位置设置android:alpha="1" 结束位置设置android:alpha="0.1" MotionLayout就会帮我们自动过度,效果如下

2. 设置个旋转属性

  <ConstraintSet android:id="@+id/start">
        <Constraint
            android:id="@+id/box"
            android:layout_width="64dp"
            android:layout_height="64dp"
            android:rotationX="0"
            ……约束条件同上/>

    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
        <Constraint
            android:id="@+id/box"
            android:layout_width="64dp"
            android:layout_height="64dp"
           ……约束条件同上/>

    </ConstraintSet>

在动画开始位置设置android:rotationX="0"结束位置设置android:rotationX="0"

你还可以改变其它属性试试,比如设置不同的背景颜色、设置不同大小等

动画的运动过程 🏃

上面的动画,都是我们定义了开始与结束两个状态,然后它自己过度的动画。下面我们来说说如果干涉他运动的过程中的

关键帧(KeyFrameSet)

KeyFrameSet是Transition的子元素
默认情况下,我们只需要定义动画开始和结束状态就行了,MotionLayout 会帮我们平滑的过度。如果我想搞一个复杂些的运动,就需要KeyFrameSet来设置,我们在动画运动过程中的某些点设置特定的约束或属性,点与点之前的过度,MotionLayout同样会帮我们平滑的过度。
KeyFrameSet 中可以包含KeyPosition、KeyAttribute、KeyCycle、KeyTimeCycle、KeyTrigger,这几种使用编辑添加如下图所示

我们点击选中①,下面会出现一个时间轴的工具,我们点击②处播放按钮就可以在编辑器预览的动画效果,我们点击③吃就可以看到 添加KeyFrameSet 的子元素的选项,大家可以动手试一试,下面以xml的方式讲解这几种元素


KeyPosition

可以改变动画运动过程中的位置。
上面我们实现的运动都是直线运行,下面我用KeyPosition来实现曲线运动

图中的菱形点就是我们用KeyPosition 修改的点,代码如下

  <Transition
        motion:constraintSetEnd="@+id/end"
        motion:constraintSetStart="@id/start"
        motion:duration="1000">

        <OnClick motion:targetId="@id/box"/>
        <KeyFrameSet>
            <KeyPosition
                motion:framePosition="50"
                motion:motionTarget="@id/box"
                motion:keyPositionType="parentRelative"
                motion:percentY="0.3"
                motion:percentX="0.8"/>

        </KeyFrameSet>
    </Transition>

在上面给KeyPosition 添加了几个属性
motion:framePosition表示动画的进度,取值范围为[0,100]。上面写的50就表示动画进度执行50%的地方。
motion:motionTarget表示修改路径的视图id
motion:keyPositionType 表示坐标系类型,取值可以是parentRelative、pathRelative、deltaRelative
motion:percentY 和  motion:percentX 就是相对参考系的纵向和横向的比例。

重点说一下keyPositionType
1.parentRelative
表示以MotionLayout 布局为参考系,布局左上角为(0,0),右下角为(1,1) 那么motion:percentX="0.8",motion:percentY="0.3"就是(0.3,0.8)的位置,如下图所示



上面我画了 两个图,按我们上面说的(0.3,0.8)应该是图1两条灰色的焦点,但我们发现图1和实际的点有些偏差。我们用KeyPosition 指定点时,是指定视图的中心点,图1是理想状态,如果我们的视图无限小,那就是图1 情况。事实上我们视图不可能无限小,真实的情况应该是图2 所示。

2.deltaRelative
 此类型下,视图的起始点坐标为(0,0), 终点坐标为(1,1),示意图如图所示

在这里插入图片描述
  <KeyFrameSet>
        <KeyPosition
            motion:framePosition="50"
            motion:motionTarget="@id/box"
            motion:keyPositionType="deltaRelative"
            motion:percentY="0.5"
            motion:percentX="0.8"
            />

    </KeyFrameSet>

这次故意将视图终点移动右上角,不然按上面的定义,起始位置和终点位置在统一水平线上,在deltaRelative 下X轴和Y轴就重合了。

3.pathRelative

这个就有意思了,起始点(0,0)是还是视图开始位置,视图的终点位置是(1,0),那么坐标系建立如下

Y轴正方向是,X轴顺时针90度的方向,和Android 中的View坐标系一个味道,就是和我们在学习学的是反的。

KeyAttribute

keyAttribute 它可以让我们改变在动画的过程中某个时刻的属性
例:

 <Transition
    motion:constraintSetEnd="@+id/end"
    motion:constraintSetStart="@id/start"
    motion:duration="2000">

    <OnClick motion:targetId="@id/box"/>
        <KeyFrameSet>
            <KeyAttribute
               motion:framePosition="50"
                motion:motionTarget="@id/box"
                android:rotationY="180"
                android:alpha="0"
                />

        </KeyFrameSet>
</Transition>

上面我们在KeyFrameSet添加了一个KeyAttribute,并指定了一个属性android:rotationY="180",其运行效果如下


可以看到视图先沿Y从0旋转到180,然后再从180 旋转到0 结束,透明的也随着动画进度进行,由1到0,再0到1。
你还可以尝试改变其实属性值试一试。

KeyAttibute 定义的是的Android 中View 原生属性,支持的属性如下android:visibility, android:alpha, android:elevation, android:rotation, android:rotationX, android:rotationY, android:scaleX, android:scaleY, android:translationX, android:translationY, android:translationZ如果是自定义的属性,可以使用KeyAttibute 的子元素CustomAttribute

<KeyFrameSet>
            <KeyAttribute
                android:rotationY="180"
                motion:framePosition="50"
                motion:motionTarget="@id/box">

                <CustomAttribute
                    motion:attributeName="rectangleColor"
                    motion:customColorValue="@color/colorAccent" />

            </KeyAttribute>
        </KeyFrameSet>

CustomAttribute需要指定两个属性,attributeName 自定义属性的名字,另一个是属性值,可以是颜色、整数、浮点数、字符串、尺寸、布尔值,上面自定义属性是颜色,所以就是使用motion:customColorValue属性来指定其值。


KeyCycle

可以让视图在动画的过程中按照一些周期函数,周期性的改变其属性值

在这里插入图片描述


上面演示是是在动画[0%,80%] 这个过程中以sin这个周期函数,周期性的改变  android:translationY 属性

 <Transition
        motion:constraintSetEnd="@+id/end"
        motion:constraintSetStart="@id/start"
        motion:duration="2000">

        <OnClick />
        <KeyFrameSet>
            <KeyCycle
                motion:framePosition="80"
                motion:motionTarget="@+id/box"
                motion:wavePeriod="1"
                motion:waveShape="sin"
                android:translationY="50dp"/>

        </KeyFrameSet>
    </Transition>

motion:framePosition="80" 表示KeyCycle 作用范围到动画的80%,motion:wavePeriod 表示运动的周期数,motion:waveShape 表示周期的类型,这是指定的是sin,就会按sin周期函数变化。


KeyTimeCycle

在关键帧上按照一些周期函数,周期性的改变其属性值,效果如下图所示

可以看到 KeyTimeCycle与KeyCycle比较,KeyTimeCycle在帧上做周期性,KeyCycle是在动画过程中做周期性

<Transition
        motion:constraintSetEnd="@+id/end"
        motion:constraintSetStart="@id/start"
        motion:duration="2000">

        <OnClick />
        <KeyFrameSet>
            <KeyTimeCycle
                motion:motionTarget="@+id/box"
                motion:wavePeriod="1"
                android:translationY="50dp"
                />

        </KeyFrameSet>
    </Transition>

参数和KeyCycle 类似。如果我们指定motion:wavePeriod 的周期数越大,变化频率就会加快。

KeyTrigger

在动画的过程中可以触发视图中的函数,例如
我们自定义一个View,,在这个类中我们定义两个公开的函数 who()where(),代码如下(最简单的自定义View 😄 )

class MyTextView(context: Context?, attrs: AttributeSet?) : AppCompatTextView(context, attrs) {

    fun who() {
        text = "我是谁"
    }

    fun where() {
        text = "我在那"
    }
}

布局代码如下

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
    ……>
    <View android:id="@+id/box"…… />

    <com.wkk.motionlayoutdemo.MyTextView
  android:id="@+id/myTextView"
  …… />

</androidx.constraintlayout.motion.widget.MotionLayout>


MotionScene 核心代码

 <Transition
        motion:constraintSetEnd="@+id/end"
        motion:constraintSetStart="@id/start"
        motion:duration="2000">
        <OnClick />
        <KeyFrameSet>
            <KeyTrigger
                motion:framePosition="20"
                motion:motionTarget="@id/myTextView"
                motion:onCross="who" />
            <KeyTrigger
                motion:framePosition="80"
                motion:motionTarget="@id/myTextView"
                motion:onCross="where" />
        </KeyFrameSet>
    </Transition>

framePosition 动画的进度,motionTarget 作用的目标, motion:onCross要触发的函数名

onCross 是动画不管是正向还是反向,只要到达设置的framePosition 就会执行函数,还有两个函数也会触发,    onPositiveCross  只有正向执行动画是到达设置的framePosition 才会执行,而 onNegativeCros 则反之。

上面的定义,就是在动画进度20%的时候触发id 为myTextView 视图的who方法,动画进度在80%的时候触发id 为myTextView 视图的where方法,其运行效果如下。

写在最后


MotionLayout 功能强大,但相关知识点也很多,上面只是对一些属性简单的演示,要想实现复杂的动画,还需要多加练习。MotionLayout 有很好的编辑器,刚开始学的时候XML 并不太会写,可以借助图形化的编辑工具点一点,看看它帮我们生成的xml是什么样的,下面我也粘贴了一些参考链接以供学习,MotionLayout 一起学起来吧。


参考链接

MotionLayout 官方文档
MotionLayout/ConstraintLayout 官方示例 (GitHub)
使用 MotionLayout 为 Android 应用添加动画效果 (Codelab)
MotionLayout API


~ FIN ~



推荐阅读
【Kotlin协程】Channel 与 Flow 深入解析
FragmentFactory 在 Koin 中的应用
打造一个 Kotlin Flow 版的 EventBus
kotlin协程:并发 & 线程安全


加好友拉你进群,技术干货聊不停


↓关注公众号↓↓添加微信交流↓



您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存